package icasue.base.lock.redis;

import icasue.base.lock.AbstractDistributedLock;
import icasue.base.lock.expecion.ServiceTimeOutException;
import icasue.base.utils.check.CommonCheck;
import icasue.base.utils.wareable.FieldConvertUtil;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.InitializingBean;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.UUID;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/11/3 下午3:49
 * @UpdateDate:
 * @Description:
 */
@Slf4j
@Data
@NoArgsConstructor
public class JedisDistributedLock extends AbstractDistributedLock implements InitializingBean {

    private Object jedisPool;
    private Class<?> jedisClassMetadata;
    private MethodHandle getResourceHandle;
    private MethodHandle closeResourceHandle;
    private MethodHandle isConnectedHandle;
    private MethodHandle evalHandle;
    private MethodHandle getHandle;
    private MethodHandle lowLevelDatasourceClearHandle;

    /**
     * If used high level jedis client. such as 3.1.0
     */
    private static Boolean highLevelClient = Boolean.TRUE;
    private static final Long SUCCESS = Long.valueOf(1);
    private static final Long FAILED = Long.valueOf(0);
    private static final MethodHandles.Lookup lookup = FieldConvertUtil.methodLookup;


    public JedisDistributedLock(Object jedisPool){
        this.jedisPool = jedisPool;
    }

    public JedisDistributedLock(Object jedisPool,String lockPrefix,Long defaultLockTimeOutMs){
        // notice: this template is not as a final template instance, just as a mode to clone.
        super();
        this.jedisPool = jedisPool;
        if(!CommonCheck.nullStrings(lockPrefix)){
            super.CONCURRENCY_KEY_PREFIX = lockPrefix;
        }
        if(defaultLockTimeOutMs != null && defaultLockTimeOutMs.longValue() != 0L){
            super.DEFAULT_TIMEOUT = String.valueOf(defaultLockTimeOutMs);
        }
    }

    private static final String LUA_LOCK = String.join(
            Strings.EMPTY,
            "local confirmUnLockedState = redis.call('GET',KEYS[1]); " ,
            " if(confirmUnLockedState) then ",
            "    return 0; ",
            " else  " ,
            "     redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]); " +
            "     return 1;  ",
            "end;"
    );

    private static final String LUA_UNLOCK = String.join(
            Strings.EMPTY,
            "local confirmCurrentThreadLock = redis.call('GET',KEYS[1]); ",
            " if(confirmCurrentThreadLock and confirmCurrentThreadLock == ARGV[1]) then ",
            " redis.call('DEL',KEYS[1]); return 1; " ,
            " else return 0; end; "
    );

    @Override
    public String tryLock(String serviceKey) {
        if(CommonCheck.nullStrings(serviceKey)){
            throw new RuntimeException("Params 'serviceKey' in necessary when invoking JedisDistributedLock's method named tryLock.");
        }
        return this.tryDoLock(
                String.join(Strings.EMPTY,CONCURRENCY_KEY_PREFIX,serviceKey),
                null,DEFAULT_TIMEOUT,null,null
        );
    }

    @Override
    public String tryLock(String serviceKey,Long timeMs) {
        if(CommonCheck.nullStrings(serviceKey) || timeMs == null || timeMs.longValue() == 0L){
            throw new RuntimeException("Params 'serviceKey', 'timeMs' is necessary when invoking JedisDistributedLock's method named tryLock.");
        }
        return this.tryDoLock(
                String.join(Strings.EMPTY,CONCURRENCY_KEY_PREFIX,serviceKey),
                null,String.valueOf(timeMs),null,null
        );
    }

    private String tryDoLock(String serviceKey,String credential,String timeMs,Object redis,Boolean returned){
        if(credential == null){
            credential = UUID.randomUUID().toString().replace("-","");
        }
        if(returned == null){
            returned = Boolean.FALSE;
        }
        Object jedis = redis;
        try {
            if(jedis == null || !(boolean)isConnectedHandle.bindTo(jedis).invoke()) {
                jedis = getResourceHandle.bindTo(jedisPool).invoke();
            }
        }catch (Throwable e){
            throw new RuntimeException("JedisDistributedLock :: tryDoLock get Redis connection resource occur an error.",e.getCause());
        }
        try {
            try {
                String locked = (String)getHandle.bindTo(jedis).invoke(serviceKey);
                if(locked != null){
                    return this.tryDoLock(serviceKey,credential,timeMs,jedis,returned);
                }else {
                    Object eval = evalHandle.bindTo(jedis).invoke(LUA_LOCK, 1,new String[]{serviceKey, credential, timeMs});
                    if(eval.equals(FAILED)){
                        return this.tryDoLock(serviceKey,credential,timeMs,jedis,returned);
                    }
                }
            }catch (Throwable e){
                // 这里因为是递归调用，因此一旦在树的内部抛异常，
                // 此时有三种情况: 1.application资源耗尽，2.redis资源耗尽连接超时, 3.redis服务超时异常
                // 后两种情况下在最外层finally 会抛出, 应用资源耗尽不处理。
            }
        }finally {
            if(jedis != null && !returned){
                try {
                    if((boolean)isConnectedHandle.bindTo(jedis).invoke()){
                        closeResourceHandle.bindTo(jedis).invoke();
                    }
                    if(!highLevelClient){
                        //clear datasource which current jedis client binding, for low level jedis Client.
                        lowLevelDatasourceClearHandle.bindTo(jedis).invoke(null);
                    }
                }catch (Throwable e){
                    throw new RuntimeException("JedisDistributedLock :: tryDoLock :: Jedis resource closing occur an error.", e.getCause());
                }
            }
        }
        return credential;
    }

    @Override
    public void unlock(String serviceKey,String credential) throws ServiceTimeOutException {
        if(CommonCheck.nullStrings(serviceKey)){
            throw new RuntimeException("Params 'serviceKey' is necessary when invoking JedisDistributedLock's method named unlock.");
        }else {
            serviceKey = String.join(Strings.EMPTY,CONCURRENCY_KEY_PREFIX,serviceKey);
        }
        Object jedis = null;
        try {
            jedis = getResourceHandle.bindTo(jedisPool).invoke();
        }catch (Throwable e){
            throw new RuntimeException("JedisDistributedLock :: unlock getJedisConnection from pool occur an error.",e.getCause());
        }
        try {
            Object eval = null;
            try {
                eval = evalHandle.bindTo(jedis).invoke(LUA_UNLOCK, 1, new String[]{serviceKey, credential});
            }catch (Throwable e){
                throw new RuntimeException("JedisDistributedLock :: unlock failed, execute eval occur an error.",e.getCause());
            }
            if(!eval.equals(SUCCESS)){
                //lock timeout or bad credential.
                // 能走到这里说明 持有的凭证确实是真实的锁，
                // 但可能因为redis 执行lua过久(网络原因)，或是redis资源耗尽(服务原因)
                // 或是业务本身执行时间过长，业务超时。因此这里抛出ServiceTimeOutException. 供用户捕获或是向外抛出 业务回滚。
                throw new ServiceTimeOutException("JedisDistributedLock :: unlock failed, execute service timeout or credential is illegal! " +
                        "\n Maybe redis is blocking!  serviceId: " + serviceKey);
            }
        }finally {
            if(jedis != null){
                try {
                    if((boolean)isConnectedHandle.bindTo(jedis).invoke()){
                        closeResourceHandle.bindTo(jedis).invoke();
                    }
                    if(!highLevelClient){
                        //clear datasource which current jedis client binding, for low level jedis Client.
                        lowLevelDatasourceClearHandle.bindTo(jedis).invoke(null);
                    }
                }catch (Throwable e){
                    throw new RuntimeException("JedisDistributedLock :: unlock :: Jedis resource closing occur an error.", e.getCause());
                }
            }
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {

        Class<?> jedisPoolClassMetadata = null;
        Class<?> abstractPoolClassMetadata = null;
        Class<?> lowLevelDatasourceClassMetadata = null;

        try {
            //high level jedis client such as 3.1.0 we suggested.
            jedisPoolClassMetadata = Class.forName("redis.clients.jedis.JedisPool");
            abstractPoolClassMetadata = Class.forName("redis.clients.jedis.util.Pool");
            jedisClassMetadata = Class.forName("redis.clients.jedis.Jedis");
        }catch (Throwable e){
            // low level jedis client 2.x.x, such as 2.8.0
            // due to when closing jedis resource in jedis 2.x maybe throw Resource has been returned.
            // so we need to change jedis's state before close. or clear the datasource which current client binding.
            try {
                jedisPoolClassMetadata = Class.forName("redis.clients.jedis.JedisPool");
                abstractPoolClassMetadata = Class.forName("redis.clients.util.Pool");
                jedisClassMetadata = Class.forName("redis.clients.jedis.Jedis");
                lowLevelDatasourceClassMetadata = abstractPoolClassMetadata;
                highLevelClient = Boolean.FALSE;
            }catch (Throwable e2){
                throw new RuntimeException("Occur an error when JedisDistributedLock init , " +
                        "details: class metadata of JedisPool, Pool<T>, Jedis  not found. ");
            }finally {
                log.info(" \n\n ############ For JedisDistributedLock, we required strongly to use high level JedisClient such as 3.1.0 \n");
            }
        }
        if(!CommonCheck.nullObjects(jedisPoolClassMetadata,abstractPoolClassMetadata,jedisClassMetadata,jedisPool)){
            if(!jedisPool.getClass().isAssignableFrom(jedisPoolClassMetadata)){
                throw new RuntimeException("The instance of jedisPool which been given is not as a implement of "
                        + jedisPoolClassMetadata.toString());
            }
            // Getting all invoke methods handle.
            MethodHandle checkPoolIfClosedHandle = null;
            try{
                //Getting method handle for checking jedisPool is open state.
                checkPoolIfClosedHandle = lookup.findVirtual(abstractPoolClassMetadata, "isClosed", MethodType.methodType(boolean.class));
                //Getting method handle for get jedis resource from jedisPool.
                getResourceHandle = lookup.findVirtual(jedisPoolClassMetadata,"getResource",MethodType.methodType(jedisClassMetadata));
                //Getting method handle for close jedis resource actually return the resource to pool.
                closeResourceHandle = lookup.findVirtual(jedisClassMetadata,"close",MethodType.methodType(void.class));
                //Getting method handle for check if current jedis is connected state.
                isConnectedHandle = lookup.findVirtual(jedisClassMetadata,"isConnected",MethodType.methodType(boolean.class));
                //Getting method handle for jedis invoke lua script by eval method.
                evalHandle = lookup.findVirtual(jedisClassMetadata,"eval",MethodType.methodType(Object.class,
                        Arrays.asList(String.class,int.class, String[].class)));
                //Getting method handle for jedis's get method.
                getHandle = lookup.findVirtual(jedisClassMetadata,"get",MethodType.methodType(String.class,String.class));
                //low level datasource setter handle getting, for clear datasource which low level jedis client binding.
                if(!highLevelClient){
                    Field jedisDataSourceField = jedisClassMetadata.getDeclaredField("dataSource");
                    jedisDataSourceField.setAccessible(true);
                    lowLevelDatasourceClearHandle = lookup.unreflectSetter(jedisDataSourceField);
                }
            }catch (Throwable e){
                throw new RuntimeException("JedisDistributedLock init error when getting methodHandles process.");
            }

            //confirm the jedisPool which supplied is opening.
            try {
                Object invoke = checkPoolIfClosedHandle.bindTo(jedisPool).invoke();
                if(invoke.equals(true)){
                    throw new RuntimeException("JedisDistributedLock init error, the jedisPool which given is closed. ");
                }
            }catch (Throwable e){
                throw new RuntimeException("JedisDistributedLock init error, occur an error when checking jedisPool's alive state ");
            }

        }else {
            throw new RuntimeException("JedisDistributedLock init need a instance of JedisPool. ");
        }
    }
}
